package com.tools.common.io;

import com.tools.common.exception.IORuntimeException;
import com.tools.common.object.Note;
import org.apache.commons.compress.archivers.sevenz.SevenZOutputFile;
import org.yaml.snakeyaml.Yaml;

import java.io.*;
import java.net.URL;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * 文件操作和 IO 操作的工具类
 * */
@Note("文件操作和 IO 操作的工具类")
public final class FileKit {

    private FileKit() {}

    @Note("后缀名与 MIME 的类型的集合")
    private static volatile Properties MIMES;

    @Note("外部 MIME 类型 properties 文件的相对路径")
    private static final String SOURCE_FILE = "custom/mime.properties";

    /* ********************************************************************************************
     *
     *          MiME 类型的相关操作
     *
     * *********************************************************************************************
     * */


    @Note("根据文件名称获取 MIME 类型，注意 file 一定要是个文件")
    public static String getMimeType(File file) {
        if(file == null) return "";
        return getMimeType(file.getName());
    }


    @Note("根据文件名称，或者其后缀名获取 MIME 类型。注意后缀名最前面需要带【.】，如：【.txt】")
    public static String getMimeType(String filenameOrSuffix) {
        if(filenameOrSuffix == null || filenameOrSuffix.isEmpty()) return "";
        private_lazyLoadMimeTypeProperties();
        if(filenameOrSuffix.charAt(0) == '.') {
            return MIMES.getProperty(filenameOrSuffix);
        }
        return MIMES.getProperty(getFileSuffix(filenameOrSuffix));
    }


    @Note("获取总 MIME 类型的个数")
    public static int mimeTypeSize() {
        return (MIMES == null) ? 0 : MIMES.size();
    }



    @Note("移除 MIME 类型的容器内容，让 JVM 释放这部分空间")
    public static void removeMimeTypePropertiesQuote() {
        if(MIMES == null) return;
        synchronized (FileKit.class) {
            if(MIMES == null) return;
            MIMES = null;
        }
    }


    /* ********************************************************************************************
     *
     *          增删文件或目录操作
     *
     * *********************************************************************************************
     * */



    @Note("判断目录是否为空")
    public static boolean dirIsEmpty(String path) {
        if(path == null || path.isEmpty()) return false;
        File file = new File(path);
        return dirIsEmpty(file);
    }


    @Note("判断目录是否为空")
    public static boolean dirIsEmpty(File dir) {
        if(dir == null) return false;
        if(!dir.exists()) return false;
        String[] filenameList = dir.list();
        return filenameList == null || filenameList.length == 0;
    }


    @Note("如果目标路径的文件或目录不存在，就一路创建")
    public static void create(String path) {
        if(path == null || path.isEmpty()) return;
        File file = new File(path);
        create(file);
    }


    @Note("如果目标路径的文件或目录不存在，就一路创建")
    public static void create(File fileOrDir) {
        if(fileOrDir == null) return;
        if(fileOrDir.exists()) {
            return;
        }
        File parent = fileOrDir.getParentFile();
        parent.mkdirs();
        String name = fileOrDir.getName();
        if(name.contains(".")) {
            try {
                fileOrDir.createNewFile();
            } catch (IOException e) {
                throw new IORuntimeException(e);
            }
            return;
        }
        fileOrDir.mkdirs();
    }


    @Note("递归删除某个路径下的所有文件，包括自己最后也会被删除")
    public static void delete(String path) {
        if(path == null || path.isEmpty()) return;
        File file = new File(path);
        delete(file);
    }


    @Note("递归删除某个路径下的所有文件，包括自己最后也会被删除")
    public static void delete(File fileOrDir) {
        if(fileOrDir == null) return;
        File[] files = fileOrDir.listFiles();
        if (fileOrDir.isFile() || files == null || files.length == 0) {
            fileOrDir.delete();
            return;
        }
        for (File f : files) {
            delete(f);
        }
        fileOrDir.delete();
    }


    @Note("将 source 路径的内容拷贝到 target 路径中")
    public static void copy(String sourcePath, String targetPath) {
        if(sourcePath == null || sourcePath.isEmpty()) return;
        if(targetPath == null || targetPath.isEmpty()) return;
        copy(new File(sourcePath), new File(targetPath));
    }


    @Note("将 source 路径中的内容拷贝到 里")
    public static void copy(String sourcePath, File target) {
        if(sourcePath == null || sourcePath.isEmpty()) return;
        if(target == null) return;
        copy(new File(sourcePath), target);
    }


    @Note("将 source 的内容拷贝到 target 路径中")
    public static void copy(File source, String targetPath) {
        if(source == null) return;
        if(targetPath == null || targetPath.isEmpty()) return;
        copy(source, new File(targetPath));
    }

    @Note("将 source 的内容拷贝到 target")
    public static void copy(File source, File target) {
        if(source == null || target == null) return;
        if(!source.exists()) return;
        if(!target.exists()) {
            target.getParentFile().mkdirs();
        }
        String name = target.getName();
        //如果 source 为 File
        if(source.isFile()) {
            if(name.contains(".")) {
                private_copyHandle(IOKit.fileInputStream(source), IOKit.fileOutputStream(target));
            } else {
                private_copyHandle(IOKit.fileInputStream(source), IOKit.fileOutputStream(new File(target, source.getName())));
            }
            return;
        }
        //如果 source 为目录
        if(name.contains(".")) throw new IORuntimeException("拷贝操作失败，源 File 是一个目录，而目标 File 是一个文件");
        File[] files = source.listFiles();
        if(files == null) return;
        for (File f : files) {
            copy(f, new File(target, f.getName()));
        }
    }


    @Note("将 source 路径的内容移动到 target 路径中")
    public static void move(String sourcePath, String targetPath) {
        if(sourcePath == null || sourcePath.isEmpty()) return;
        if(targetPath == null || targetPath.isEmpty()) return;
        move(new File(sourcePath), new File(targetPath));
    }


    @Note("将 source 路径中的内容移动到 里")
    public static void move(String sourcePath, File target) {
        if(sourcePath == null || sourcePath.isEmpty()) return;
        if(target == null) return;
        move(new File(sourcePath), target);
    }


    @Note("将 source 的内容移动到 target 路径中")
    public static void move(File source, String targetPath) {
        if(source == null) return;
        if(targetPath == null || targetPath.isEmpty()) return;
        move(source, new File(targetPath));
    }


    @Note("将 source 的移动到 target 中")
    public static void move(File source, File target) {
        if(source == null || target == null) return;
        if(!source.exists()) return;
        if(!target.exists()) {
            target.getParentFile().mkdirs();
        }
        String name = target.getName();
        //如果 source 为 File
        if(source.isFile()) {
            if(name.contains(".")) {
                private_copyHandle(IOKit.fileInputStream(source), IOKit.fileOutputStream(target));
            } else {
                private_copyHandle(IOKit.fileInputStream(source), IOKit.fileOutputStream(new File(target, source.getName())));
            }
            source.delete();
            return;
        }
        //如果 source 为目录
        if(name.contains(".")) throw new IORuntimeException("拷贝操作失败，源 File 是一个目录，而目标 File 是一个文件");
        File[] files = source.listFiles();
        if(files != null && files.length != 0) {
            for (File f : files) {
                move(f, new File(target, f.getName()));
            }
            source.delete();
        }
    }


    /* ********************************************************************************************
     *
     *          文件或目录等路径、名称、后缀等数据的获取操作
     *
     * *********************************************************************************************
     * */


    @Note("默认使用 UTF-8 字符集编码字符串")
    public static String encoding(String str) {
        return encoding(str, CharsetEnum.UTF_8);
    }


    @Note("使用指定的字符集编码字符串")
    public static String encoding(String str, CharsetEnum charset) {
        if(charset == null) throw new IllegalArgumentException("编码字符串时，所需的字符集的枚举参数不能为 null");
        try {
            return URLEncoder.encode(str, charset.getValue());
        } catch (UnsupportedEncodingException e) {
            throw new IORuntimeException(e);
        }
    }


    @Note("默认使用 UTF-8 字符集解码字符串")
    public static String decoding(String str) {
        return decoding(str, CharsetEnum.UTF_8);
    }


    @Note("使用指定的字符集解码字符串")
    public static String decoding(String str, CharsetEnum charset) {
        if(charset == null) throw new IllegalArgumentException("解码字符串时，所需的字符集的枚举参数不能为 null");
        try {
            return URLDecoder.decode(str, charset.getValue());
        } catch (UnsupportedEncodingException e) {
            throw new IORuntimeException(e);
        }
    }

    @Note("把路径的 \\ 符合改为 / ")
    public static String pathSymbolConversion(String path) {
        return (path == null) ? ""
                : path.replace("\\", "/");
    }


    @Note("解析 【classpath:】 开头的路径为当前项目的类路径，以此获取真实的物理路径")
    public static String getRealPath(String path) {
        if (path == null || path.isEmpty()) return "";
        path = pathSymbolConversion(path);
        if (path.startsWith("classpath:")) {
            String classPath = getClassPath();
            return classPath + "/" + (path.replace("classpath:", ""));
        }
        return path;
    }


    @Note("当前项目的类路径，就是编译后类的存放目录路径，如：【D:/project/target/classes】 ")
    public static String getClassPath() {
        String path = FileKit.class.getResource("/").toString()
                .replace("file:/", "")
                .replace("\\", "/");
        char ch = path.charAt(path.length() - 1);
        if(ch == '/' || ch == '\\') {
            return path.substring(0, path.length() - 1);
        }
        return path;
    }


    @Note("获取本项目编译后类文件夹中的指定文件的绝对路径")
    public static String getResourceFilePath(String filename) {
        if(filename == null || filename.isEmpty()) return getClassPath();
        int start;
        if((start = filename.lastIndexOf("/")) != -1) {
            filename = filename.substring(start);
        }
        if(filename.startsWith("classpath:")) {
            filename = filename.replace("classpath:", "");
        }
        URL path = FileKit.class.getClassLoader().getResource(filename);
        return (path == null) ? "" : path.toString()
                .replace("file:/", "")
                .replace("\\", "/");
    }


    @Note("获取本项目编译后类文件夹中的指定文件的绝对路径，然后构建对应的文件实例返回")
    public static File getResourceFile(String filename) {
        return new File(getResourceFilePath(filename));
    }


    @Note("获取本项目编译后类文件夹中的指定文件的字节缓冲输入流 BufferedInputStream 返回")
    public static InputStream getResourceInputStream(String filename) {
        if(filename == null || filename.isEmpty()) {
            return FileKit.class.getClassLoader().getResourceAsStream("");
        }
        int start;
        if((start = filename.lastIndexOf("/")) != -1) {
            filename = filename.substring(start);
        }
        if(filename.startsWith("classpath:")) {
            filename = filename.replace("classpath:", "");
        }
        return FileKit.class.getClassLoader().getResourceAsStream(filename);
    }


    @Note("获取文件的文件名称，但是去除【.后缀名】部分")
    public static String getPureFilename(File file) {
        if(file == null) return "";
        String name = file.getName();
        int i = name.lastIndexOf(".");
        return (i < 0) ? name : name.substring(0, i);
    }


    @Note("获取文件的 【.后缀名称】 ")
    public static String getFileSuffix(String pathOrFilename) {
        if(pathOrFilename == null || pathOrFilename.isEmpty()) return "";
        int index = pathOrFilename.lastIndexOf(".");
        return (index < 0) ? "" : pathOrFilename.substring(index);
    }


    @Note("获取文件的 【后缀名称】，不要那个【.】")
    public static String getPureFileSuffix(String pathOrFilename) {
        String suffix = getFileSuffix(pathOrFilename);
        return (suffix.isEmpty()) ? "" : suffix.substring(1);
    }


    @Note("判断目标字符串是否是文件的后缀名")
    public static boolean isFileSuffix(String suffix) {
        if(suffix == null || suffix.isEmpty()) return false;
        return suffix.charAt(0) == '.';
    }


    @Note("判断 File 实例是否是文件")
    public static boolean isFile(File file) {
        if(file == null) return false;
        String name = file.getName();
        return name.contains(".");
    }


    @Note("判断 File 实例是否是目录")
    public static boolean isDirectory(File file) {
        if(file == null) return false;
        String name = file.getName();
        return !name.contains(".");
    }

    /* ********************************************************************************************
     *
     *          读取 yml 文件，返回一个 Map 对象
     *
     * *********************************************************************************************
     * */


    @Note("根据传入的 Reader 字符输入流读取目标 yml 文件，并封装为 key 是 xxx.xxx， value 是具体值的 Map 集合")
    public static Map<String, Object> readYml(Reader reader) {
        Yaml yaml = new Yaml();
        Map<String, Object> result = new HashMap<>();
        Map map = yaml.loadAs(reader, HashMap.class);
        private_loopReadYml(map, "", result);
        return result;
    }


    @Note("根据传入的 InputStream 字节流读取目标 yml 文件，并封装为 key 是 xxx.xxx， value 是具体值的 Map 集合")
    public static Map<String, Object> readYml(InputStream input) {
        Yaml yaml = new Yaml();
        Map<String, Object> result = new HashMap<>();
        Map map = yaml.loadAs(input, HashMap.class);
        private_loopReadYml(map, "", result);
        return result;
    }



    @Note("根据传入的绝对路径来读取目标 yml 文件，并封装为 key 是 xxx.xxx， value 是具体值的 Map 集合")
    public static Map<String, Object> readYml(String ymlPath) {
        Yaml yaml = new Yaml();
        Map<String, Object> result = new HashMap<>();
        Map map = yaml.loadAs(IOKit.fileReader(ymlPath), HashMap.class);
        private_loopReadYml(map, "", result);
        return result;
    }



    @Note("根据传入的文件对象 File 来读取目标 yml 文件，并封装为 key 是 xxx.xxx， value 是具体值的 Map 集合")
    public static Map<String, Object> readYml(File yml) {
        Yaml yaml = new Yaml();
        Map<String, Object> result = new HashMap<>();
        Map map = yaml.loadAs(IOKit.fileReader(yml), HashMap.class);
        private_loopReadYml(map, "", result);
        return result;
    }

    /* ********************************************************************************************
     *
     *          读取 properties 文件，返回一个 Properties 对象
     *
     * *********************************************************************************************
     * */


    @Note("读取文件实例，加载内容生成 Properties 对象")
    public static Properties readProperties(File file) {
        return private_readToProperties(IOKit.fileReader(file));
    }


    @Note("读取文件路径获取 Properties 对象")
    public static Properties readProperties(String filePath) {
        return private_readToProperties(IOKit.fileReader(filePath));
    }


    @Note("通过字符输入流 Reader 获取 Properties 对象")
    public static Properties readProperties(Reader reader) {
        return private_readToProperties(reader);
    }


    @Note("通过字节输入流 InputStream 获取 Properties 对象")
    public static Properties readProperties(InputStream input) {
        return private_readToProperties(input);
    }


    /* ********************************************************************************************
     *
     *          压缩包操作
     *
     * *********************************************************************************************
     * */

    @Note("压缩指定的所有文件成 ZIP 压缩包")
    public static void createZip(ZipBean zipBean) {
        if(zipBean == null) throw new NullPointerException();
        if(!zipBean.useCompress()) throw new IllegalArgumentException("压缩操作缺少压缩包保存路径或压缩文件");
        ZipOutputStream zipOutput = zipBean.getCompressStream();
        try {
            for (File file : zipBean.getFiles()) {
                private_zipPut(null, file, zipOutput);
            }
        } finally {
            IOKit.finish(zipOutput);
            IOKit.close(zipOutput);
        }
    }


    @Note("解压缩 ZIP 文件到指定目录")
    public static void unZip(ZipBean zipBean) {
        if(zipBean == null) throw new NullPointerException();
        if(!zipBean.useUnCompress()) throw new IllegalArgumentException("解压缩操作缺少文件保存路径或源压缩包文件");
        ZipInputStream zipInput = zipBean.getUnCompressStream();
        try {
            ZipEntry zipEntry;
            while ((zipEntry = zipInput.getNextEntry()) != null) {
                File file = new File(zipBean.getUnZipSavePath(), zipEntry.getName());
                if(zipEntry.isDirectory()) {
                    file.mkdirs();
                    return;
                }
                file.getParentFile().mkdirs();
                byte[] buffs = new byte[IOKit.DEFAULT_BUFFERED_BYTE_SIZE];
                int length;
                FileOutputStream output = new FileOutputStream(file);
                while ((length = zipInput.read(buffs)) != -1) {
                    output.write(buffs, 0, length);
                    output.flush();
                }
                output.close();
            }
        } catch (IOException e) {
            throw new IORuntimeException(e);
        } finally {
            IOKit.close(zipInput);
        }
    }

    /* ********************************************************************************************
     *
     *          私有逻辑
     *
     * *********************************************************************************************
     * */

    @Note("懒加载后缀名与 MIME 类型的 Properties 容器")
    private static void private_lazyLoadMimeTypeProperties() {
        if(MIMES != null) return;
        synchronized (FileKit.class) {
            if(MIMES != null) return;
            InputStream inputStream = getResourceInputStream(SOURCE_FILE);
            MIMES = readProperties(inputStream);
        }
    }

    @Note("递归扫描 yml 并装载数据生成的 Map，然后 put 进结果 Map 里")
    private static void private_loopReadYml(Map map, String parentKey, Map<String, Object> result) {
        parentKey = "".equals(parentKey) ? parentKey : parentKey + ".";
        Object key;
        Object value;
        for(Object o : map.entrySet()) {
            Map.Entry e = (Map.Entry) o;
            key = e.getKey();
            String name = parentKey + ((key == null) ? null : key.toString());
            value = e.getValue();
            if(value instanceof Map) {
                private_loopReadYml((Map) value, name, result);
                continue;
            }
            result.put(name, value);
        }
    }


    @Note("根据入参不同的流来加载 Properties 内容，并返回装载完毕数据的 Properties 容器对象")
    private static Properties private_readToProperties(Object inputStreamOrReader) {
        Properties properties = new Properties();
        InputStream input = null;
        Reader reader = null;
        try {
            if(inputStreamOrReader instanceof InputStream) {
                input = (InputStream) inputStreamOrReader;
                properties.load(input);
            } else {
                reader = (Reader) inputStreamOrReader;
                properties.load(reader);
            }
        } catch (IOException e) {
            throw new IORuntimeException(e, "读取 Properties 配置文件出错");
        } finally {
            IOKit.close(input);
            IOKit.close(reader);
        }
        return properties;
    }

    @Note("检查压缩包的保存路径和要压缩的文件")
    private static void private_checkZipSavePath(File zipSavePath, int length) {
        if(zipSavePath == null) throw new NullPointerException("压缩包保存路径不能为 null");
        if(length == 0) throw new NullPointerException("压缩包至少需要压缩一个文件");
        FileKit.create(zipSavePath);
        if(zipSavePath.isDirectory()) throw new IORuntimeException("压缩包的保存路径缺少压缩包的文件名称");
    }

    @Note("7z 递归检索目录和文件并压缩的逻辑")
    private static void private_7zPut(String parentFilePath, File file, SevenZOutputFile out) throws IOException {
        if (!file.exists()) return;
        String entryName = (parentFilePath == null)
                ? file.getName()
                : parentFilePath + "/" + file.getName();
        if(file.isDirectory()) {
            File[] childFiles = file.listFiles();
            if(childFiles == null) return;
            for (File childFile : childFiles) {
                private_7zPut(entryName, childFile, out);
            }
            return;
        }
        out.putArchiveEntry(out.createArchiveEntry(file, entryName));
        out.write(new FileInputStream(file));
        out.closeArchiveEntry();
    }


    @Note("zip 压缩文件的过程，使用递归的方式实现")
    private static void private_zipPut(String parentFilePath, File file, ZipOutputStream zipOutput) {
        String zipEntryName = (parentFilePath == null)
                ? ""
                : parentFilePath + "/" + file.getName();
        if (file.isDirectory()) {
            File[] childFiles = file.listFiles();
            if(childFiles == null) return;
            for (File childFile : childFiles) {
                private_zipPut(zipEntryName, childFile, zipOutput);
            }
            return;
        }
        FileInputStream input = null;
        int len;
        byte[] buffs = new byte[IOKit.DEFAULT_BUFFERED_BYTE_SIZE];
        try {
            input = new FileInputStream(file);
            zipOutput.putNextEntry(new ZipEntry(zipEntryName));
            while ((len = input.read(buffs)) != -1) {
                zipOutput.write(buffs, 0, len);
                zipOutput.flush();
            }
        } catch (IOException e) {
            throw new IORuntimeException(e);
        } finally {
            IOKit.close(input);
            IOKit.closeEntry(zipOutput);
        }
    }


    @Note("文件流的拷贝")
    private static void private_copyHandle(InputStream input, OutputStream output) {
        int len;
        byte[] bys = new byte[IOKit.DEFAULT_BUFFERED_BYTE_SIZE];
        try {
            while ((len = input.read(bys)) != -1) {
                output.write(bys, 0, len);
                output.flush();
            }
        } catch (IOException e) {
            throw new IORuntimeException(e);
        } finally {
            IOKit.close(input);
            IOKit.close(output);
        }
    }
}
